iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
1
Modern Web

用Elixir學習後端煉金術系列 第 23

Day 23 |> Phoenix |> 使用 Ecto 存取資料

  • 分享至 

  • xImage
  •  

(承上篇)

如同其他 web 框架的 Model 類別,通過建立 schema 模組我們可以做到對資料庫資料的存取。

今天我們可能需要一個對於文章資料表做存取的 schema,透過以下的指令,我們可以建立 post schema。

$ mix phx.gen.schema Post posts title:string content:string price:integer
* creating lib/sample_project/post.ex
* creating priv/repo/migrations/20201008132804_create_posts.exs

然後,就會創建一個 SampleProject.Post 這個模組。

lib/sample_project/post.ex

defmodule SampleProject.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :content, :string
    field :price, :integer
    field :title, :string

    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :content, :price])
    |> validate_required([:title, :content, :price])
  end
end

然後,會建立一個遷移檔案,這個檔案紀錄了資料庫的架構變更,在專案遷移時,可以直接完成資料庫及相關資料表的建立,在這個檔案中,定義資料表相關資料,就可以進行新增資料表及欄位新增:

defmodule SampleProject.Repo.Migrations.CreatePosts do
  use Ecto.Migration

  def change do
    create table(:posts) do
      add :title, :string
      add :content, :string
      add :price, :integer

      timestamps()
    end

  end
end

然後,輸入以下指令就可以進行遷移。

$ mix ecto.migrate

https://ithelp.ithome.com.tw/upload/images/20201008/20111629ww5wIAtVA1.png


變更集 (ChangeSet)

在 Post schema 中,可以看到 changeset 這個函式,他的功能是在資料準備存入資料庫前,所需要進行的資料轉換或是驗證,因此變更集可以允許我們用更彈性的方式來處理資料,並過濾掉錯誤的資料。

lib/sample_project/post.ex

  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :content, :price])
    |> validate_required([:title, :content])
  end

Ecto.Changeset.cast/3 的功能是用來進行資料的轉換及欄位的過濾,它接受三個參數,第一個參數是 %Post{} ,是以 Post Module 本身所定義的 struct 本身,而第二個參數是一個 map 代表資料本身要進行的轉換,第三個參數是允許通過的欄位所構成的 list。

以下面幾個範例來講:
因為content跟price都允許通過,寫map為空,所以資料不會有所改變。

iex> Ecto.Changeset.cast(%SampleProject.Post{content: "test", price: 100}, %{}, [:content, :price]) 
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
 data: #SampleProject.Post<>, valid?: true>

有一個 price 映射到20的轉換,但因為只允許 content 欄位進行轉換,所以資料不會改變。

iex> Ecto.Changeset.cast(%SampleProject.Post{content: "test", price: 100}, %{"price" => 20}, [:content]) 
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
 data: #SampleProject.Post<>, valid?: true>

price 欄位資料轉換為20。

iex> Ecto.Changeset.cast(%SampleProject.Post{content: "test", price: 100}, %{"price" => 20}, [:price])   
#Ecto.Changeset<
  action: nil,
  changes: %{price: 20},
  errors: [],
  data: #SampleProject.Post<>,
  valid?: true

cast/3進行的任務就是資料的轉換,不會進行驗證,所以可以看到其output的 struct 中,valid? 都是 true。

change/2cast/3功能很接近,但少了 cast 的過濾功能,當資料來源可信時,也是可以只用 change 即可。

驗證
Ecto.Changeset內提供了多驗證方法,可以查找這邊

iex> changeset = Post.changeset(%Post{}, %{title: "test title"}) 
#Ecto.Changeset<
  action: nil,
  changes: %{title: "test title"},
  errors: [
    content: {"can't be blank", [validation: :required]},
    price: {"can't be blank", [validation: :required]}
  ],
  data: #SampleProject.Post<>,
  valid?: false

iex> changeset.valid?
false

iex> changeset.errors
[
  content: {"can't be blank", [validation: :required]},
  price: {"can't be blank", [validation: :required]}
]

若是能夠成功變更(新增)資料,應該會得到一個valid為true的結果,並且一個很方便的地方是,能夠從change直接查看到所發生改變的欄位值。

iex> changeset = Post.changeset(%Post{}, %{title: "test title", content: "test content", price: 18}) 
#Ecto.Changeset<
  action: nil,
  changes: %{content: "test content", price: 18, title: "test title"},
  errors: [],
  data: #SampleProject.Post<>,
  valid?: true

新增數據

Repo.insert這個函式可以對資料庫新增數據

iex> alias SampleProject.Repo 
SampleProject.Repo
iex> Repo.insert(%Post{title: "test title"})
[debug] QUERY OK db=16.0ms decode=16.0ms idle=125.0ms
INSERT INTO "posts" ("title","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["test title", ~N[2020-10-08 16:13:53], ~N[2020-10-08 16:13:53]]
{:ok,
 %SampleProject.Post{
   __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
   content: nil,
   id: 1,
   inserted_at: ~N[2020-10-08 16:13:53],
   price: nil,
   title: "test title",
   updated_at: ~N[2020-10-08 16:13:53]
 }}

https://ithelp.ithome.com.tw/upload/images/20201009/20111629A3gFbQ8aFG.png

透過變更集,我們可以在新增資料時進行驗證抑或資料轉換:

iex> changeset = Post.changeset(%Post{}, %{title: "test title", content: "content"}) 
#Ecto.Changeset<
  action: nil,
  changes: %{content: "content", title: "test title"},
  errors: [price: {"can't be blank", [validation: :required]}],
  data: #SampleProject.Post<>,
  valid?: false
>
iex> {:error, changeset} = Repo.insert(changeset)                                    
{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{content: "content", title: "test title"},
   errors: [price: {"can't be blank", [validation: :required]}],
   data: #SampleProject.Post<>,
   valid?: false
 >}

iex> changeset = Post.changeset(%Post{}, %{title: "test title", content: "content", price: 100}) 
#Ecto.Changeset<
  action: nil,
  changes: %{content: "content", price: 100, title: "test title"},
  errors: [],
  data: #SampleProject.Post<>,
  valid?: true
>
iex> {:error, changeset} = Repo.insert(changeset)
[debug] QUERY OK db=16.0ms idle=109.0ms
INSERT INTO "posts" ("content","price","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" ["content", 100, "test title", ~N[2020-10-08 16:33:36], ~N[2020-10-08 16:33:36]]
** (MatchError) no match of right hand side value: {:ok, %SampleProject.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, content: "content", id: 2, inserted_at: ~N[2020-10-08 16:33:36], price: 100, title: "test title", updated_at: ~N[2020-10-08 16:33:36]}}

取得資料

或是Repo.all函式,可以抓出所有數據。

iex> Repo.all(Post) 
[debug] QUERY OK source="posts" db=0.0ms idle=141.0ms
SELECT p0."id", p0."content", p0."price", p0."title", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 [] 
[
  %SampleProject.Post{
    __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
    content: nil,
    id: 1,
    inserted_at: ~N[2020-10-08 16:13:53],
    price: nil,
    title: "test title",
    updated_at: ~N[2020-10-08 16:13:53]
  }
]

或是Repo.one可以僅抓出一個數據

iex> Repo.one(Post) 
[debug] QUERY OK source="posts" db=15.0ms idle=422.0ms
SELECT p0."id", p0."content", p0."price", p0."title", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 [] 
%SampleProject.Post{
  __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
  content: nil,
  id: 1,
  inserted_at: ~N[2020-10-08 16:13:53],
  price: nil,
  title: "test title",
  updated_at: ~N[2020-10-08 16:13:53]
}

allone的差別在於是否用一個list回傳結構資料。


條件查詢

藉由使用Ecto.Query.from,我們可以建立條件查詢,並且自己定義回傳的資料結構於select對應的值。

iex> import Ecto.Query
Ecto.Query
iex> Repo.all(from u in Post, select: u.title)
[debug] QUERY OK source="posts" db=0.0ms idle=656.0ms
SELECT p0."title" FROM "posts" AS p0 []
["test title"]

iex> Repo.all(from u in Post, select: [title: u.title, content: u.content]) 
[debug] QUERY OK source="posts" db=0.0ms idle=781.0ms
SELECT p0."title", p0."content" FROM "posts" AS p0 []
[[title: "test title", content: nil]]

總結,Ecto的詳細用法,可以參閱官方文件
體驗過後,個人覺得 Ecto 設計算是十分精妙,也用了更漂亮的語法來完成資料查詢。
可以感覺到 Elixir 雖然在社群大小上還不算非常龐大,但在工具的成熟度上,感覺也完全不會輸其他語言的工具。


上一篇
Day 22 |> Phoenix |> 目錄結構
下一篇
Day 24 |> Phoenix |> Router
系列文
用Elixir學習後端煉金術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言